#! /usr/bin/env python
"""Simple JSON-RPC Server.

This module can be used to create simple JSON-RPC servers
by creating a server and either installing functions, a
class instance, or by extending the SimpleJSONRPCServer
class.

It can also be used to handle JSON-RPC requests in a CGI
environment using CGIJSONRPCRequestHandler.

A list of possible usage patterns follows:

1. Install functions:

server = SimpleJSONRPCServer(("localhost", 8000))
server.register_function(pow)
server.register_function(lambda x,y: x+y, 'add')
server.serve_forever()

2. Install an instance:

class MyFuncs:
    def __init__(self):
        # make all of the string functions available through
        # string.func_name
        import string
        self.string = string
    def _listMethods(self):
        # implement this method so that system.listMethods
        # knows to advertise the strings methods
        return list_public_methods(self) + \
                ['string.' + method for method in list_public_methods(self.string)]
    def pow(self, x, y): return pow(x, y)
    def add(self, x, y) : return x + y

server = SimpleJSONRPCServer(("localhost", 8000))
server.register_introspection_functions()
server.register_instance(MyFuncs())
server.serve_forever()

3. Install an instance with custom dispatch method:

class Math:
    def _listMethods(self):
        # this method must be present for system.listMethods
        # to work
        return ['add', 'pow']
    def _methodHelp(self, method):
        # this method must be present for system.methodHelp
        # to work
        if method == 'add':
            return "add(2,3) => 5"
        elif method == 'pow':
            return "pow(x, y[, z]) => number"
        else:
            # By convention, return empty
            # string if no help is available
            return ""
    def _dispatch(self, method, params):
        if method == 'pow':
            return pow(*params)
        elif method == 'add':
            return params[0] + params[1]
        else:
            raise 'bad method'

server = SimpleXMLRPCServer(("localhost", 8000))
server.register_introspection_functions()
server.register_instance(Math())
server.serve_forever()

4. Subclass SimpleJSONRPCServer:

class MathServer(SimpleJSONRPCServer):
    def _dispatch(self, method, params):
        try:
            # We are forcing the 'export_' prefix on methods that are
            # callable through JSON-RPC to prevent potential security
            # problems
            func = getattr(self, 'export_' + method)
        except AttributeError:
            raise Exception('method "%s" is not supported' % method)
        else:
            return func(*params)

    def export_add(self, x, y):
        return x + y

server = MathServer(("localhost", 8000))
server.serve_forever()

5. CGI script:

server = CGIJSONRPCRequestHandler()
server.register_function(pow)
server.handle_request()
"""

# This implementation was converted from SimpleXMLRPCServer by
# David McNab (david@rebirthing.co.nz)

# Original SimpleXMLRPCServer module was written by Brian
# Quinlan (brian@sweetapp.com), Based on code written by Fredrik Lundh.

#TODO: correct this import
from SocketServer import ThreadingMixIn
from jsonrpclib import *
import BaseHTTPServer
import SocketServer
import os
import sys
import threading
import traceback
try:
    import fcntl
except ImportError:
    fcntl = None


def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
    """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d

    Resolves a dotted attribute name to an object.  Raises
    an AttributeError if any attribute in the chain starts with a '_'.

    If the optional allow_dotted_names argument is false, dots are not
    supported and this function operates similar to getattr(obj, attr).
    """

    if allow_dotted_names:
        attrs = attr.split('.')
    else:
        attrs = [attr]

    for i in attrs:
        if i.startswith('_'):
            raise AttributeError(
                'attempt to access private attribute "%s"' % i
                )
        else:
            obj = getattr(obj, i)
    return obj

def list_public_methods(obj):
    """Returns a list of attribute strings, found in the specified
    object, which represent callable attributes"""

    return [member for member in dir(obj)
                if not member.startswith('_') and
                    callable(getattr(obj, member))]

def remove_duplicates(lst):
    """remove_duplicates([2,2,2,1,3,3]) => [3,1,2]

    Returns a copy of a list without duplicates. Every list
    item must be hashable and the order of the items in the
    resulting list is not defined.
    """
    u = {}
    for x in lst:
        u[x] = 1

    return u.keys()

def _record_rr(meth):
    
    def _call(*args, **dict_arg):
        
        data = args[1]
        self = args[0]
        ret = meth(*args, **dict_arg)
        _last_requet_response = (data, ret)
        return ret

    return _call

class JSONRPCDispatcherMixIn:
    """Mix-in class that dispatches JSON-RPC requests.
    Based on SimpleJSONRPCDispatcher, but overrides
    _marshaled_dispatch for JSON-RPC

    This class is used to register JSON-RPC method handlers
    and then to dispatch them. There should never be any
    reason to instantiate this class directly.
    """
    
    def __init__(self, data_serializer, echo_on=False):
        self.funcs = {}
        self.instance = None
        self.__data_serializer = data_serializer
        self._echo_on = echo_on

    def register_instance(self, instance, allow_dotted_names=True):
        """Registers an instance to respond to JSON-RPC requests.

        Only one instance can be installed at a time.

        If the registered instance has a _dispatch method then that
        method will be called with the name of the JSON-RPC method and
        its parameters as a tuple
        e.g. instance._dispatch('add',(2,3))

        If the registered instance does not have a _dispatch method
        then the instance will be searched to find a matching method
        and, if found, will be called. Methods beginning with an '_'
        are considered private and will not be called by
        SimpleJSONRPCServer.

        If a registered function matches a JSON-RPC request, then it
        will be called instead of the registered instance.

        If the optional allow_dotted_names argument is true and the
        instance does not have a _dispatch method, method names
        containing dots are supported and resolved, as long as none of
        the name segments start with an '_'.

            *** SECURITY WARNING: ***

            Enabling the allow_dotted_names options allows intruders
            to access your module's global variables and may allow
            intruders to execute arbitrary code on your machine.  Only
            use this option on a secure, closed network.

        """

        self.instance = instance
        self.allow_dotted_names = allow_dotted_names


    def register_function(self, function, name=None):
        """Registers a function to respond to JSON-RPC requests.

        The optional name argument can be used to set a Unicode name
        for the function.
        """

        if name is None:
            name = function.__name__
        self.funcs[name] = function


    def register_introspection_functions(self):
        """Registers the JSON-RPC introspection methods in the system
        namespace.

        see http://JSONrpc.usefulinc.com/doc/reserved.html
        """

        self.funcs.update({'system.listMethods' : self.system_listMethods,
                      'system.methodSignature' : self.system_methodSignature,
                      'system.methodHelp' : self.system_methodHelp})
    
    
    def register_multicall_functions(self):
        """Due to the specification this multicall has not be implemented"""
        raise NotImplementedError(self.__class__.__name__ + '.register_multicall_functions()')
    
    
    def echo_on(self, on=True):
        self._echo_on = on    
    
    
    @_record_rr
    def _marshaled_dispatch(self, data, dispatch_method=None):
        """Dispatches a JSON-RPC method from marshalled (JSON) data.
    
        JSON-RPC methods are dispatched from the marshalled (JSON) data
        using the _dispatch method and the result is returned as
        marshalled data. For backwards compatibility, a dispatch
        function can be provided as an argument (see comment in
        SimpleJSONRPCRequestHandler.do_POST) but overriding the
        existing method through subclassing is the prefered means
        of changing method dispatch behavior.
        """
        
        notification = False
        try:
            req = self.__data_serializer.loads_request(data)
            if len(req) == 2:       #notification
                method, params = req
                notification = True
            else:                   #request
                method, params, id = req
        except RPCFault, err:
            return self.__data_serializer.dumps_error(err, id=None)
        except Exception, err:
            #TODO: log it
#            self.log("%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)))
            print "1 %d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err))
            return self.__data_serializer.dumps_error(RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id=None)
        
        # generate response
        try:
            if self._echo_on and method == 'system.echo' :
                result = {'method' : method,
                          'parameters' : params,
                          'raw data': data, }
            elif dispatch_method is not None:
                result = dispatch_method(method, params)
            else:
                result = self._dispatch(method, params)
            ## wrap response in a singleton tuple
            #response = (response,)
            #response = JSONrpclib.dumps(response, methodresponse=1)
        except RPCFault, err:
            if notification:
                return None
            return self.__data_serializer.dumps_error("%s:%s" % (sys.exc_type, sys.exc_value), id=None)
        except Exception, err:
            if notification:
                return None
            #TODO: log it
#            self.log("%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)))
            print "2 %d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err))
            return self.__data_serializer.dumps_error(RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id)
    
        if notification:
            return None
        try:
            return self.__data_serializer.dumps_response(result, id)
        except Exception, err:
            #TODO: log it
#            self.log("%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)))
            print "3 %d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err))
            return self.__data_serializer.dumps_error(RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id)


    def system_listMethods(self):
        """system.listMethods() => ['add', 'subtract', 'multiple']

        Returns a list of the methods supported by the server."""

        methods = self.funcs.keys()
        if self.instance is not None:
            # Instance can implement _listMethod to return a list of
            # methods
            if hasattr(self.instance, '_listMethods'):
                methods = remove_duplicates(
                        methods + self.instance._listMethods()
                    )
            # if the instance has a _dispatch method then we
            # don't have enough information to provide a list
            # of methods
            elif not hasattr(self.instance, '_dispatch'):
                methods = remove_duplicates(
                        methods + list_public_methods(self.instance)
                    )
        methods.sort()
        return methods


    def system_methodSignature(self, method_name):
        """system.methodSignature('add') => [double, int, int]

        Returns a list describing the signature of the method. In the
        above example, the add method takes two integers as arguments
        and returns a double result.

        This server does NOT support system.methodSignature."""

        return 'signatures not supported'


    def system_methodHelp(self, method_name):
        """system.methodHelp('add') => "Adds two integers together"

        Returns a string containing documentation for the specified method."""

        method = None
        if self.funcs.has_key(method_name):
            method = self.funcs[method_name]
        elif self.instance is not None:
            # Instance can implement _methodHelp to return help for a method
            if hasattr(self.instance, '_methodHelp'):
                return self.instance._methodHelp(method_name)
            # if the instance has a _dispatch method then we
            # don't have enough information to provide help
            elif not hasattr(self.instance, '_dispatch'):
                try:
                    method = resolve_dotted_attribute(
                                self.instance,
                                method_name,
                                self.allow_dotted_names
                                )
                except AttributeError:
                    pass

        # Note that we aren't checking that the method actually
        # be a callable object of some kind
        if method is None:
            return ""
        else:
            import pydoc
            return pydoc.getdoc(method)


    def system_multicall(self, call_list):
        raise NotImplementedError(self.__class__.__name__ + '. system_multicall()')


    def _dispatch(self, method, params):
        """Dispatches the JSON-RPC method.

        JSON-RPC calls are forwarded to a registered function that
        matches the called JSON-RPC method name. If no such function
        exists then the call is forwarded to the registered instance,
        if available.

        If the registered instance has a _dispatch method then that
        method will be called with the name of the JSON-RPC method and
        its parameters as a tuple
        e.g. instance._dispatch('add',(2,3))

        If the registered instance does not have a _dispatch method
        then the instance will be searched to find a matching method
        and, if found, will be called.

        Methods beginning with an '_' are considered private and will
        not be called.
        """

        func = None
        try:
            # check to see if a matching function has been registered
            func = self.funcs[method]
        except KeyError:
            if self.instance is not None:
                # check for a _dispatch method
                if hasattr(self.instance, '_dispatch'):
                    return self.instance._dispatch(method, params)
                else:
                    # call instance method directly
                    try:
                        func = resolve_dotted_attribute(
                            self.instance,
                            method,
                            self.allow_dotted_names
                            )
                    except AttributeError:
                        pass

        if func is not None:
            if isinstance(params, dict):
                return func(**params)
            else:
                return func(*params)
        else:
            raise Exception('method "%s" is not supported' % method)


class Server(JSONRPCDispatcherMixIn):
    """RPC-server.

    It works with different data/serializers and 
    with different transports.

    :Example:
        see module-docstring

    :TODO:
        - mixed JSON-RPC 1.0/2.0 server?
        - logging/loglevels?
    """
    def __init__(self, data_serializer, transport, logfile=None):
        """
        :Parameters:
            - data_serializer: a data_structure+serializer-instance
            - transport: a Transport instance
            - logfile: file to log ("unexpected") errors to
        """
        JSONRPCDispatcherMixIn.__init__(self, data_serializer)
        self.__data_serializer = data_serializer
        if not isinstance(transport, Transport):
            raise ValueError('invalid "transport" (must be a Transport-instance)"')
        self.__transport = transport
        self.logfile = logfile
        if self.logfile is not None:    #create logfile (or raise exception)
            f = codecs.open(self.logfile, 'a', encoding='utf-8')
            f.close()

        self.funcs = {}

    def __repr__(self):
        return "<Server for %s, with serializer %s>" % (self.__transport, self.__data_serializer)

    def log(self, message):
        """write a message to the logfile (in utf-8)"""
        if self.logfile is not None:
            f = codecs.open(self.logfile, 'a', encoding='utf-8')
            f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + message + "\n")
            f.close()

    def serve(self, n=None):
        """serve (forever or for n communicaions).
        
        :See: Transport
        """
        self.__transport.serve(self._marshaled_dispatch, n)
        
    
class SimpleJSONRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    """Simple JSON-RPC request handler class.

    Handles all HTTP POST requests and attempts to decode them as
    JSON-RPC requests.
    """

    # Class attribute listing the accessible path components;
    # paths not on this list will result in a 404 error.
    rpc_paths = ('/', '/RPC2')

    def is_rpc_path_valid(self):
        if self.rpc_paths:
            return self.path in self.rpc_paths
        else:
            # If .rpc_paths is empty, just assume all paths are legal
            return True
    
    def do_POST(self):
        """Handles the HTTP POST request.
    
        Attempts to interpret all HTTP POST requests as JSON-RPC calls,
        which are forwarded to the server's _dispatch method for handling.
        """
        # Check that the path is legal
        if not self.is_rpc_path_valid():
            self.report_404()
            return
        
        try:
            # Get arguments by reading body of request.
            # We read this in chunks to avoid straining
            # socket.read(); around the 10 or 15Mb mark, some platforms
            # begin to have problems (bug #792570).
            max_chunk_size = 10 * 1024 * 1024
            size_remaining = int(self.headers["content-length"])
            L = []
            while size_remaining:
                chunk_size = min(size_remaining, max_chunk_size)
                L.append(self.rfile.read(chunk_size))
                size_remaining -= len(L[ - 1])
            data = ''.join(L)
            # In previous versions of SimpleJSONRPCServer, _dispatch
            # could be overridden in this class, instead of in
            # SimpleJSONRPCDispatcher. To maintain backwards compatibility,
            # check to see if a subclass implements _dispatch and dispatch
            # using that method if present.
            response = self.server._marshaled_dispatch(
                    data, getattr(self, '_dispatch', None)
                )
        except Exception, why: # This should only happen if the module is buggy
            # internal error, report as HTTP server error
            print why
            self.send_response(500)
            self.end_headers()
        else:
            # got a valid JSON RPC response
            self.send_response(200)
            self.send_header("Content-type", "application/json-rpc")
            self.send_header("Content-length", str(len(response)))
            self.end_headers()
            self.wfile.write(response)
    
            # shut down the connection
            self.wfile.flush()
            self.connection.shutdown(1)
      
            
    def report_404 (self, response='No such page'):
            # Report a 404 error
        self.send_response(404)
        self.send_header("Content-type", "text/plain")
        self.send_header("Content-length", str(len(response)))
        self.end_headers()
        self.wfile.write(response)
        # shut down the connection
        self.wfile.flush()
        self.connection.shutdown(1)


    def log_request(self, code='-', size='-'):
        """Selectively log an accepted request."""
        if self.server.logRequests:
            BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
            
    
class SimpleJSONRPCServer(SocketServer.TCPServer,
                         JSONRPCDispatcherMixIn):
    """Simple JSON-RPC server.

    Simple JSON-RPC server that allows functions and a single instance
    to be installed to handle requests. The default implementation
    attempts to dispatch JSON-RPC calls to the functions or instance
    installed in the server. Override the _dispatch method inhereted
    from SimpleJSONRPCDispatcher to change this behavior.
    """
    allow_reuse_address = True
    
    def __init__(self, addr, data_serializer=JsonRpc20(),
                requestHandler=SimpleJSONRPCRequestHandler,
                logRequests=True):
        self.logRequests = logRequests

        JSONRPCDispatcherMixIn.__init__(self, data_serializer)
        SocketServer.TCPServer.__init__(self, addr, requestHandler)
        
        # [Bug #1222790] If possible, set close-on-exec flag; if a
        # method spawns a subprocess, the subprocess shouldn't have
        # the listening socket open.
        if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
            flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
            flags |= fcntl.FD_CLOEXEC
            fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)


class CGIJSONRPCRequestHandler(JSONRPCDispatcherMixIn):
    """Simple handler for JSON-RPC data passed through CGI."""
    def __init__(self):
        JSONRPCDispatcherMixIn.__init__(self)
    
    def handle_get(self):
        """Handle a single HTTP GET request.
    
        Default implementation indicates an error because
        JSON-RPC uses the POST method.
        """
    
        code = 400
        message, explain = \
                 BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
    
        response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
            {
             'code' : code,
             'message' : message,
             'explain' : explain
            }
        print 'Status: %d %s' % (code, message)
        print 'Content-Type: text/html'
        print 'Content-Length: %d' % len(response)
        print
        sys.stdout.write(response)
    
    def handle_request(self, request_text=None):
        """Handle a single JSON-RPC request passed through a CGI post method.
    
        If no JSON data is given then it is read from stdin. The resulting
        JSON-RPC response is printed to stdout along with the correct HTTP
        headers.
        """
        if request_text is None and \
            os.environ.get('REQUEST_METHOD', None) == 'GET':
            self.handle_get()
        else:
            # POST data is normally available through stdin
            if request_text is None:
                request_text = sys.stdin.read()
    
            self.handle_jsonrpc(request_text)
    
    def handle_jsonrpc(self, request_text):
        """Handle a single JSON-RPC request"""
    
        response = self._marshaled_dispatch(request_text)
    
        print 'Content-Type: application/json-rpc'
        print 'Content-Length: %d' % len(response)
        print
        sys.stdout.write(response)
        

class ThreadingJSONRPCServerMixIn(SimpleJSONRPCServer):
    
    allow_reuse_address = 1    # Seems to make sense in testing environment
    
    def __init__(self, *args, **dic_args):
        
        SimpleJSONRPCServer.__init__(self, *args, **dic_args)
        self._shutdown_evt = None
        
    def start(self): 
        if self._shutdown_evt and not self._shutdown_evt.isSet() :
            raise Exception('Current server is running, can not restart it!')
        
        self._shutdown_evt = threading.Event()
        
        while True :
            self.handle_request()
            self._shutdown_evt.wait(0)
            # leave and stop severing
            if self._shutdown_evt.isSet() : break
        
#        logger.debug('RPCServer is shutdown')
            
    def shutdown(self): 
        if self._shutdown_evt :
            # set the shutdown event
            self._shutdown_evt.set()
            try :
                # push it handle a request
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
                sock.settimeout(0)
                sock.connect(self.server_address)
            except : pass
            finally :
                sock.close()
            # close the current server
            self.server_close()
            

class ThreadingPoolMixIn:
    """Mix-in class to handle each request in a new thread in thread pool."""
    
    def process_request_thread(self, request, client_address):
        """Same as in BaseServer but as a thread.

        In addition, exception handling is done here.

        """
        try:
            self.finish_request(request, client_address)
            self.close_request(request)
        except:
            self.handle_error(request, client_address)
            self.close_request(request)

    def process_request(self, request, client_address):
        """Start a new thread to process the request."""
        import cn.pigersing.threadpool as threadpool
        rqst = threadpool.makeRequests(self.process_request_thread, [request, client_address])
        self.threadpool.putRequest(rqst)
        

class SimpleThreadingJSONRPCServer(ThreadingMixIn, ThreadingJSONRPCServerMixIn):pass


class ThreadPoolJSONRPCServer(ThreadingMixIn, ThreadingJSONRPCServerMixIn):
    
    def __init__(self, threadpool, *args, **dic_args):
        import cn.pigersing.threadpool as TP
        ThreadingJSONRPCServerMixIn.__init__(self,*args, **dic_args)
        self.threadpool = threadpool or TP.ThreadPool(5)
        
    
if __name__ == '__main__':
    try:
        server = SimpleThreadingJSONRPCServer(("localhost", 8000))
        server.register_function(pow)
        server.register_function(lambda x, y: x + y, 'add')
        def returnNone():
            print 'Return None'
        server.register_function(returnNone)
        server.echo_on()
        th = threading.Thread(target = server.start)
        th.setDaemon(True)
        th.start()
        import jsonrpclib
        transport = jsonrpclib.TransportHTTPRequest("http://localhost:8000/")
        serializer = jsonrpclib.JsonRpc20()
        s = jsonrpclib.ServerProxy(serializer, transport)
        c = s.add(2, 3)
        print 'called add, and returned:', c
        ret =  s.system.echo('English')
        print ret
        for param in ret['parameters']:
            print param
        print s.system.echo(eacho = True)
        print s.returnNone()
    finally:
        server.shutdown()
        print 'server has been closed'
